Skip to content

🐛 fix(limiter): floor sub-second expiration in sliding window - #4564

Merged
ReneWerner87 merged 7 commits into
gofiber:mainfrom
nikolauspschuetz:fix-limiter-subsecond-expiration
Jul 30, 2026
Merged

🐛 fix(limiter): floor sub-second expiration in sliding window#4564
ReneWerner87 merged 7 commits into
gofiber:mainfrom
nikolauspschuetz:fix-limiter-subsecond-expiration

Conversation

@nikolauspschuetz

Copy link
Copy Markdown

Description

SlidingWindow computes the window length as:

expiration := uint64(expirationDuration.Seconds())

Any positive sub-second ExpirationFunc value truncates to 0. The window weight is then

weight := float64(resetInSec) / float64(expiration) // / 0  -> +Inf/NaN
rate   := int(math.Ceil(float64(e.prevHits)*weight)) + e.currHits

so rate becomes garbage and every request — including the first — is rejected with 429, regardless of Max. A limiter configured with, say, ExpirationFunc: func(fiber.Ctx) time.Duration { return 500*time.Millisecond } blocks all traffic.

Fix

Floor the truncated window to 1 second (a sub-second window is treated as a 1-second window rather than breaking):

expiration := uint64(expirationDuration.Seconds())
if expiration == 0 {
    expiration = 1
}

Testing

Added TestLimiterSlidingSubSecondExpiration, which is rejected with 429 before this change and returns 200 after. go test ./middleware/limiter/ (full package), go vet, and gofmt are clean.

Note: FixedWindow shares the same uint64(...Seconds()) truncation; I scoped this PR to SlidingWindow since that's the one with a clean, demonstrable failure (the NaN-rate 429). Happy to follow up on the fixed-window window-sizing behavior separately if maintainers agree it's worth addressing.

SlidingWindow computed the expiration as uint64(expirationDuration.Seconds()),
so any positive sub-second ExpirationFunc value (e.g. 500ms) truncated to 0.
The window weight is then float64(resetInSec) / float64(expiration), a
division by zero that makes the rate NaN, so every request — including the
first — was rejected with 429 regardless of Max.

Floor the truncated window to 1 second. Adds a regression test that is
rejected with 429 before the fix and returns 200 after.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Limiter expiration normalization is centralized in windowSeconds, which defaults non-positive durations and floors positive sub-second durations to one second. Fixed and sliding windows use the helper, with regression tests and documentation covering the behavior.

Changes

Limiter expiration normalization

Layer / File(s) Summary
Expiration normalization and limiter wiring
middleware/limiter/config.go, middleware/limiter/limiter_fixed.go, middleware/limiter/limiter_sliding.go
windowSeconds normalizes durations to whole-second values used by fixed and sliding windows, with fixed-window durations derived through secondsToDuration.
Sub-second regression coverage and documentation
middleware/limiter/limiter_test.go, docs/middleware/limiter.md
Tests verify that 500ms windows accept five requests, report a one-second reset, and reject the next request; documentation describes whole-second accounting.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: gaby

Poem

A rabbit watched the limiter gate,
And floored short windows before they wait.
Five hops pass, then the sixth must stay,
Until one whole second clears the way.
Fixed and sliding now count alike.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the fix and testing but omits most required template sections, including Fixes #, Changes introduced, Type of change, and Checklist. Rewrite the PR body to follow the template: add Fixes #, fill Changes introduced, Type of change, Checklist, and any relevant docs/changelog/migration details.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: flooring sub-second limiter expiration in SlidingWindow.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ReneWerner87 ReneWerner87 added this to v3 Jul 28, 2026
@ReneWerner87 ReneWerner87 added this to the v3 milestone Jul 28, 2026
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.24%. Comparing base (9904597) to head (8bfc756).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4564      +/-   ##
==========================================
- Coverage   93.31%   93.24%   -0.07%     
==========================================
  Files         140      140              
  Lines       14858    14859       +1     
==========================================
- Hits        13864    13856       -8     
- Misses        619      627       +8     
- Partials      375      376       +1     
Flag Coverage Δ
unittests 93.24% <100.00%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gaby

gaby commented Jul 29, 2026

Copy link
Copy Markdown
Member

@nikolauspschuetz The linter workflow is failing

@ReneWerner87 ReneWerner87 self-assigned this Jul 30, 2026
uint64(expirationDuration.Seconds()) truncates a positive sub-second
ExpirationFunc value to a 0-second window. The sliding window then divides by
zero, so the rate is int(NaN) and requests within Max are rejected. The fixed
window rotates on every request instead, resets its counter each time and stops
limiting altogether, which is the same bug failing open.

Fold the non-positive fallback and the floor into one resolveExpiration helper
both strategies share. The fixed window needs the floored duration too, since it
hands it to the storage as the entry TTL, and a 500ms TTL under a 1s window would
drop hits mid-window.

The regression test only reproduced on amd64: int(NaN) wraps to MinInt64 there
but saturates to 0 on arm64, where the unfixed sliding window admitted the
request and the test passed. Assert X-RateLimit-Reset instead, which is 0 without
the floor and 1 with it on every architecture, and run it against both strategies
with an injected clock.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@middleware/limiter/config.go`:
- Around line 35-36: Align the expiration documentation with resolveExpiration:
in middleware/limiter/config.go lines 35-36, document that only positive
sub-second durations are floored to one second while zero or negative durations
use the default expiration; apply the same clarification to
docs/middleware/limiter.md lines 113-114 and update the ExpirationFunc table
description at line 135.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8e4c660f-37ac-43f2-9c53-22c2a277522b

📥 Commits

Reviewing files that changed from the base of the PR and between 101543c and 4aeffc2.

📒 Files selected for processing (5)
  • docs/middleware/limiter.md
  • middleware/limiter/config.go
  • middleware/limiter/limiter_fixed.go
  • middleware/limiter/limiter_sliding.go
  • middleware/limiter/limiter_test.go

Comment thread middleware/limiter/config.go Outdated
gocritic's unnamedResult rejected the (time.Duration, uint64) pair, and naming
the results would just trade it for nonamedreturns. Return the window seconds
only and let the fixed window derive its storage TTL through the existing
secondsToDuration helper, which keeps the TTL tied to the floored window.
The field doc and the docs page both said every value below one second floors to
one second. Zero and negative values fall back to the default expiration instead,
so name which case does what.
@ReneWerner87
ReneWerner87 merged commit de8d07d into gofiber:main Jul 30, 2026
27 checks passed
@welcome

welcome Bot commented Jul 30, 2026

Copy link
Copy Markdown

Congrats on merging your first pull request! 🎉 We here at Fiber are proud of you! If you need help or want to chat with us, join us on Discord https://gofiber.io/discord

@github-project-automation github-project-automation Bot moved this to Done in v3 Jul 30, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes limiter window sizing when ExpirationFunc returns a positive sub-second duration, preventing zero-second windows that break sliding-window math (division by zero → NaN/Inf) and aligning behavior to a minimum 1-second accounting window.

Changes:

  • Introduce windowSeconds to floor positive sub-second expirations to a 1-second window (and keep existing fallback for non-positive durations).
  • Apply the new window sizing in both sliding and fixed limiter strategies.
  • Add regression tests covering sub-second expirations and update limiter docs to describe the whole-second accounting behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
middleware/limiter/limiter_sliding.go Uses windowSeconds to ensure sliding-window expiration is never 0 seconds.
middleware/limiter/limiter_fixed.go Uses windowSeconds (and derives TTL from whole-second window) so fixed-window behavior is consistent for sub-second expirations.
middleware/limiter/config.go Documents the behavior and adds the shared windowSeconds helper.
middleware/limiter/limiter_test.go Adds regression tests for sub-second expirations for both strategies.
docs/middleware/limiter.md Documents flooring behavior for ExpirationFunc in user-facing docs.

if d <= 0 {
d = ConfigDefault.Expiration
}
if sec := uint64(d.Seconds()); sec > 0 {
Comment on lines +40 to +43
// Generate expiration from generator. The storage TTL is derived from the
// window so a sub-second value cannot expire the entry mid-window.
expiration := windowSeconds(cfg.ExpirationFunc(c))
expirationDuration, _ := secondsToDuration(expiration)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants